﻿// (c) Copyright Michael Antonio.
// This source is subject to the Microsoft Public License (Ms-PL).
// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
// All other rights reserved
using System;
using System.Linq;
using System.Xml.Linq;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Xml;
using MichMan.Utilities;
using System.Reflection;
using System.Text;

///
/// I like DataContract serialization, but it doesn't work with XML attributes.
/// And I'm not a fan of XmlSerialization.  Produces ugly code from XSD files.
/// Also, I like reflection to a fault.
/// 
/// So I rolled my own serializer (!).  It kind of uses XPath in property attributes 
/// to decide what parts of the document to load.  
/// 
/// Note that I've only written enough of this to support Subsonic.  
/// It's not what I'd call "robust".  
/// "Minimally Functional" would be more like it.
///
namespace MichMan.Utilities
{
    public class ZPathAttribute : Attribute
    {
        public ZPathAttribute()
        {
        }

        public ZPathAttribute(string path)
        {
            Path = path;
        }

        public string Path { get; set; }

        public object DefaultValue { get; set; }

        /// <summary>
        /// Override this to provide custom parsing.
        /// </summary>
        /// <param name="t"></param>
        /// <param name="value"></param>
        /// <returns></returns>
        public virtual bool TryParse(Type t, string value, out object result)
        {
            // Does this include nullable Guid?
            if (t.IsAssignableFrom(typeof(Guid)))
            {
                value = value.Replace("urn:uuid:", "");
            }

            return ParseUtility.TryParseType(t, value, out result);
        }

        /// <summary>
        /// Override this to provide custom handling.
        /// </summary>
        /// <param name="t"></param>
        /// <param name="root"></param>
        /// <returns></returns>
        public virtual object Load(Type t, XElement root)
        {
            if (this.IsOverridden("Parse"))
            {
                object result;
                if (TryParse(t, root.GetZPathValue(this.Path), out result))
                {
                    return result;
                }
                return null;
            }
            else
            {
                return ZPathSerializer.LoadProperty(t, root, this);
            }
        }
    }

    /// <summary>
    /// Use this base class to create objects that can be saved by calling "ToString" and loaded by calling "Parse".
    /// Note that you do not have to use this base class to read Xml.
    /// </summary>
    public class ZPathObject : IComparable
    {
        public int CompareTo(object obj)
        {
            if (obj == null)
            {
                return 1;
            }

            if (!obj.GetType().Equals(this.GetType()))
            {
                throw new ArgumentException("Wrong type for IComprable.CompareTo");
            }

            ZPathObject other = (ZPathObject)obj;
            if (this.Xml == null && other.Xml == null)
            {
            }

            if (this.Xml != null && other.Xml == null)
            {
                return 1;
            }

            if (this.Xml == null && other.Xml != null)
            {
                return -1;
            }

            return this.Xml.CompareTo(other.Xml);
        }

        public string Xml { get; internal set; }
        public XElement Element
        {
            get
            {
                return XElement.Parse(Xml);
            }
        }

        public override string ToString()
        {
            return Xml;
        }

        /// <summary>
        /// These are not "Save" so much as "Save Initial State".  
        /// </summary>
        /// <param name="toStream"></param>

        private void Save(Stream toStream)
        {
            Element.Save(toStream);
        }
        private void Save(XmlWriter writer)
        {
            Element.Save(writer);
        }
        private void Save(TextWriter writer)
        {
            Element.Save(writer);
        }
        private void Save(TextWriter writer, SaveOptions options)
        {
            Element.Save(writer, options);
        }
        private void Save(Stream stream, SaveOptions options)
        {
            Element.Save(stream, options);
        }

        public static T Parse<T>(string value) where T : new()
        {
            XElement xelt = XElement.Parse(value);
            return ZPathSerializer<T>.Load(xelt);
        }
    }
    
    public class ZPathSerializer
    {
        internal static ZPathAttribute GetZPathAttribute(MemberInfo mi)
        {
            ZPathAttribute xpathAttribute = mi.GetCustomAttributes(typeof(ZPathAttribute), false).Cast<ZPathAttribute>().FirstOrDefault();
            if (xpathAttribute != null && String.IsNullOrEmpty(xpathAttribute.Path))
            {
                StringBuilder builder = new StringBuilder();
                foreach (var c in mi.Name)
                {
                    builder.Append(builder.Length == 0 ? Char.ToLower(c) : c);
                }

                xpathAttribute.Path = builder.ToString();
            }
            return xpathAttribute;
        }

        public static bool IsZPathSerializable(Type type)
        {
            return type.GetCustomAttributes(typeof(ZPathAttribute), true).Any() ||
                type.GetProperties().Any(pi => pi.PropertyType.GetCustomAttributes(typeof(ZPathAttribute), true).Any() || pi.GetCustomAttributes(typeof(ZPathAttribute), true).Any());
        }

        /// <summary>
        /// Calls the generic "Load" function for an object of type t.
        /// </summary>
        /// <param name="t">Type to use when constructing the generic.</param>
        /// <param name="elt">Parameter passed into "Load"</param>
        /// <returns></returns>
        public static object ReadZPathObject(Type t, XElement elt)
        {
            Type gen = typeof(ZPathSerializer<>).MakeGenericType(new[] { t });
            object serializer = Activator.CreateInstance(gen);
            return serializer.GetType().GetMethod("Load").Invoke(null, new[] { elt });
        }

        private static bool IsGenericList(Type t)
        {
            return (t != null &&
                t.IsGenericType &&
                t.GetGenericTypeDefinition().Equals(typeof(List<>)));
        }

        /// <summary>
        /// Loads a generic list object
        /// </summary>
        /// <param name="root"></param>
        /// <param name="collectionType"></param>
        /// <param name="elementName"></param>
        /// <returns></returns>
        private static IList LoadList(XElement root, Type collectionType, string elementName)
        {
            Type elementType = null;
            bool loadInternal = false;
            if (IsGenericList(collectionType))
            {
                elementType = collectionType.GetGenericArguments().FirstOrDefault();
            }
            else if (IsGenericList(collectionType.BaseType))
            {
                elementType = collectionType.BaseType.GetGenericArguments().FirstOrDefault();
                loadInternal = true;
            }
            else
            {
                return null;
            }

            IList list = (IList)Activator.CreateInstance(collectionType);
            var elts = root.Descendants().Where(d => d.Name.LocalName == elementName); // TODO:  What if it's a compound path?
            foreach (var elt in elts)
            {
                // Get list memeber and add to list
                list.Add(ZPathSerializer.ReadZPathObject(elementType, elt));
            }

            if (loadInternal)
            {
                Type gen = typeof(ZPathSerializer<>).MakeGenericType(new[] { collectionType });
                object serializer = Activator.CreateInstance(gen);
                serializer.GetType().GetMethod("LoadInternal").Invoke(null, new object[] { list, root });
            }

            return list;
        }

        internal static object LoadProperty(Type propertyType, XElement root, ZPathAttribute xpath)
        {
            // If it's a direct list type, load the list.
            if (IsGenericList(propertyType))
            {
                return LoadList(root, propertyType, xpath.Path);
            }
            // If the property type has an XPath attributed, that would be the names of elements on the list.
            // So if it's a list type, load the list.
            else if (propertyType.GetCustomAttributes(typeof(ZPathAttribute), false).Any())
            {
                if (IsGenericList(propertyType.BaseType))
                {
                    ZPathAttribute listItems = GetZPathAttribute(propertyType);
                    XElement collection = root.GetZPath<XElement>(xpath.Path);
                    if (collection != null)
                    {
                        // Create list
                        return LoadList(collection, propertyType, listItems.Path);
                    }
                }
            }
            // If it has sub-properties with XPath attributes, then it's an object.  Call "Load" recursively.
            else if (propertyType.GetProperties().Any(p => p.GetCustomAttributes(typeof(ZPathAttribute), false).Any()))
            {
                XElement newRoot = root.GetZPath<XElement>(xpath.Path);
                if (newRoot != null)
                {
                    return ZPathSerializer.ReadZPathObject(propertyType, newRoot);
                }
            }
            // This should be the case when an object has a value
            else
            {
                string value = root.GetZPathValue(xpath.Path);
                if (!String.IsNullOrEmpty(value))
                {
                    object newValue = null;
                    if (propertyType.IsEnum)
                    {
                        newValue = Enum.Parse(propertyType, value, true);
                    }
                    else
                    {
                        if (propertyType == typeof(string))
                        {
                            newValue = value;
                        }
                        else
                        {
                            xpath.TryParse(propertyType, value, out newValue);
                        }
                    }

                    if (newValue != null)
                    {
                        return newValue;
                    }
                }
            }
            return null;
        }
    }


    public class ZPathSerializer<T> where T : new()
    {
        public static T Load(XElement root)
        {
            T obj = new T();
            if (obj is ZPathObject)
            {
                (obj as ZPathObject).Xml = root.ToString();
            }
            return LoadInternal(obj, root);
        }

        public static T LoadInternal(T obj, XElement root)
        {
            foreach (var prop in obj.GetType().GetProperties())
            {
                ZPathAttribute xpath = ZPathSerializer.GetZPathAttribute(prop);
                if (xpath != null)
                {
                    var setMethod = prop.GetSetMethod();
                    object value = xpath.Load(prop.PropertyType, root);

                    if (value == null)
                    {
                        value = xpath.DefaultValue;
                    }

                    if (value != null)
                    {
                        setMethod.Invoke(obj, new[] { value });
                    }
                }
            }
            return obj;
        }

        /// <summary>
        /// TODO: This would construct an XElement from an object by traversing the tree and using the XPath attributes.
        /// It would also include any XNode objects directly.
        /// Note sure if that would be a "complete" serializer.
        /// </summary>
        /// <param name="o"></param>
        /// <returns></returns>
        /*
        public static XElement Save(T o)
        {
            if (o is XElement || o == null)
            {
                return o as XElement;
            }

            return null;
        }
         */
    }
}